﻿using System;
using System.Linq;
using System.Collections.Generic;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

using Ah.Testudin.Engine.Actors;
using Ah.Testudin.Engine.UI.Views;
using Ah.Testudin.Engine.Graphics.Scenes;
using Ah.Testudin.Engine.Events;
using Ah.Testudin.Engine.Physics;
using Ah.Testudin.Engine.Processes;
using Ah.Testudin.Engine.Actors.Spawning;
using Ah.Testudin.Engine.Content;
using Ah.Testudin.Engine.Graphics;
using Ah.Testudin.Engine.UI.Elements.Debugging;
using Ah.Testudin.Engine.Actors.Components.Animation;
using Ah.Testudin.Engine.Actors.Components;
using Ah.Testudin.Engine.Actors.Components.Render;
using Ah.Testudin.Engine.Graphics.Scenes.SceneNodes.Cameras;
using Ah.Testudin.Engine.Actors.Components.Collision;
using Ah.Testudin.Engine.Input;
using Ah.Testudin.Engine.UI.Elements;
using Ah.Testudin.Engine.AI;

namespace Ah.Testudin.Engine.State
{
    public class InGameApplicationState : ApplicationStateBase
    {
        #region Constants

        private const float GRAVITY_ACCELERATION = 1.32f;

        private const int FLOOR_DIMENSIONS = 20;

        private const float MOVEMENT_SPEED = .1f;

        private const float MAX_SPEED = .5f;

        #endregion

        #region Private Member Variables

        private readonly int _viewPortWidth;

        private readonly int _viewPortHeight;

        private List<Actor> _actorList;

        private readonly Scene _scene;

        private readonly IEventManager _eventManager;

        private readonly IPhysicsSimulator _physicsSimulator;

        private readonly IProcessManager _processManager;

        private readonly IActorSpawner _actorSpawner;

        private readonly ITestudinContentManager _contentManager;

        #endregion

        #region Constructors

        public InGameApplicationState(
            int viewPortWidth,
            int viewPortHeight,
            IPhysicsSimulator physicsSimulator,
            IProcessManager processManager,
            IActorSpawner actorSpawner)
            : base()
        {
            _viewPortWidth = viewPortWidth;

            _viewPortHeight = viewPortHeight;

            _scene = new Scene(
                GraphicsDeviceRendererSingleton.Instance,
                EventManagerSingleton.Instance);

            _physicsSimulator = physicsSimulator;

            _physicsSimulator.GetActorDelegate = GetActor;

            _processManager = processManager;

            _actorSpawner = actorSpawner;

            _actorList = new List<Actor>();

            _eventManager = EventManagerSingleton.Instance;

            _contentManager = TestudinContentManagerSingleton.Instance;

            RegisterForEvents();
        }

        #endregion

        #region IApplicationState

        private void Initialize()
        {
            DisposeGameViews();

            DisposeActors();

            _eventManager.Initialize();

            // TODO: Dispose these next 3 guys, be a man and manage your memory!

            _scene.Initialize();

            _physicsSimulator.Initialize();

            _processManager.AbortAllProcesses(true);
        }

        public override void Load()
        {
            Initialize();

            _contentManager.Load<Model>("Content/Models/missile");

            ProfilingScreenElement.InitializeSingleton(
                new Vector2(10, 30),
                10,
                10,
                _contentManager.Load<SpriteFont>("Content/Fonts/Arial"),
                _renderer);

            DebugOutputScreenElement.InitializeSingleton(
                new Vector2(10, 50),
                10,
                10,
                _contentManager.Load<SpriteFont>("Content/Fonts/Arial"),
                _renderer);

            Texture2D backgroundTexture = _contentManager.Load<Texture2D>("Content/background");

            _processManager.AttachProcess(
                new AIChooseBestBehaviorProcess(
                    500,
                    _eventManager));

            _processManager.AttachProcess(
                new AIUpdateProcess(_eventManager));

            _processManager.AttachProcess(
                new PhysicsUpdateProcess(_eventManager));

            _processManager.AttachProcess(
                new AnimationUpdateProcess(_eventManager));

            _processManager.AttachProcess(
                new ActorOutOfBoundsProcess(
                    GetAllActors,
                    _eventManager));

            ActorId backgroundActorId = ActorIdGenerator.GetNextActorId();

            Actor backgroundActor = new Actor(backgroundActorId);

            backgroundActor.SetComponent<IRenderComponent>(
                new SpriteRenderComponent(
                    backgroundActor,
                    RenderPass.StaticActor,
                    backgroundTexture,
                    new Rectangle(
                        0,
                        0,
                        _viewPortWidth,
                        _viewPortHeight),
                    _eventManager));

            AddActor(backgroundActor);

            Actor playerActor = _actorSpawner.SpawnActor(
                ActorType.Cube,
                Matrix.CreateFromAxisAngle(
                    Vector3.Up,
                    MathHelper.PiOver2) *
                Matrix.CreateTranslation(
                    new Vector3(
                        -5,
                        0,
                        -5)));

            AddActor(playerActor);

            playerActor.SetComponent<PlayerComponent>(
                new PlayerComponent(
                    playerActor,
                    "Bullgoose"));

            playerActor.SetComponent<HealthComponent>(
                new HealthComponent(
                    100,
                    playerActor,
                    _eventManager));

            Actor aiCubeActor = _actorSpawner.SpawnActor(
                ActorType.Cube,
                Matrix.CreateTranslation(
                    new Vector3(
                        0,
                        0,
                        4)));

            aiCubeActor.SetComponent<HealthComponent>(
                new HealthComponent(
                    100,
                    aiCubeActor,
                    _eventManager));

            aiCubeActor.SetComponent<PlayerComponent>(
                new PlayerComponent(
                    aiCubeActor,
                    "Player 2"));

            AddActor(aiCubeActor);

            aiCubeActor.SetComponent<AIComponent>(
                new AIComponent(
                    aiCubeActor,
                    new AIStateMachine(
                        new HardCodedBrain(
                            aiCubeActor,
                            5000,
                            1f,
                            new List<Vector3>()
                            {
                                new Vector3(5, 0, 0),
                                new Vector3(-5, 0, 0),
                                new Vector3(0, 0, 4)

                            },
                            _eventManager)),
                    _eventManager));

            Actor primitiveActor = CreatePrimitiveActor();

            AddActor(primitiveActor);

            Actor floorActor = BuildFloor();

            AddActor(floorActor);

            SetCamera(primitiveActor.Id);

            ISceneNode primitiveSceneNode = _scene.FindActorSceneNode(primitiveActor.Id);

            ICursor mouseCursor = new MouseCursor(
                20,
                20,
                _contentManager.Load<Texture2D>("Content/Sprites/default-cursor"));

            IGameView gameView = new HumanInGameView(
                new List<IScreenElement>()
                {
                    new SceneScreenElement(
                        new Vector2(0, 0),
                        _viewPortWidth,
                        _viewPortHeight,
                        _scene),

                    new FpsScreenElement(
                        new Vector2(10, 10),
                        10,
                        10,
                        _contentManager.Load<SpriteFont>("Content/Fonts/Arial"),
                        _renderer),

                    new HealthBarScreenElement(
                        new Vector2(
                            450,
                            20),
                        300,
                        20,
                        _contentManager.Load<Texture2D>("Content/Sprites/HealthBarBackground"),
                        _contentManager.Load<Texture2D>("Content/Sprites/HealthBarForeground"),
                        aiCubeActor,
                        _renderer,
                        "Computer",
                        _contentManager.Load<SpriteFont>("Content/Fonts/Arial")),

                    new HealthBarScreenElement(
                        new Vector2(
                            450,
                            50),
                        300,
                        20,
                        _contentManager.Load<Texture2D>("Content/Sprites/HealthBarBackground"),
                        _contentManager.Load<Texture2D>("Content/Sprites/HealthBarForeground"),
                        playerActor,
                        _renderer,
                        "Bullgoose",
                        _contentManager.Load<SpriteFont>("Content/Fonts/Arial")),

                    ProfilingScreenElement.GetInstance(),

                    new ActorCountScreenElement(
                        new Vector2(80, 10),
                        10,
                        10,
                        GetAllActors,
                        _renderer,
                        _contentManager.Load<SpriteFont>("Content/Fonts/Arial"))
                },
                new FreeMovementController(
                    _scene.Camera),
                new TortoiseController(
                    _scene.FindActorSceneNode(playerActor.Id),
                    _eventManager),
                mouseCursor);

            _gameViews.Add(gameView);
        }

        public override void Update(GameTime gameTime)
        {
            _processManager.Update(gameTime);

            _physicsSimulator.Update(gameTime);

            foreach (IGameView currentGameView in _gameViews)
            {
                currentGameView.Update(gameTime);
            }

            _eventManager.Update(gameTime);
        }

        public override void RenderDiagnostics(GameTime gameTime)
        {
            _physicsSimulator.RenderDiagnostics();
        }

        #endregion

        #region Private Methods

        private void RegisterForEvents()
        {
            _eventManager.RegisterEventHandler(
                EventType.ActorAccelerating,
                HandleThrustStartedEvent);

            _eventManager.RegisterEventHandler(
                EventType.ActorDecelerating,
                HandleThrustEndedEvent);

            _eventManager.RegisterEventHandler(
                EventType.ActorMoving,
                HandleActorMovingEvent);

            _eventManager.RegisterEventHandler(
                EventType.ActorRotating,
                HandleActorRotatingEvent);

            _eventManager.RegisterEventHandler(
                EventType.ActorStopped,
                HandleActorMovingEvent);

            _eventManager.RegisterEventHandler(
                EventType.ActorSpawning,
                HandleActorSpawningEvent);

            _eventManager.RegisterEventHandler(
                EventType.Explosion,
                HandleExplosionEvent);

            _eventManager.RegisterEventHandler(
                EventType.Collision,
                HandleCollisionEvent);

            _eventManager.RegisterEventHandler(
                EventType.ActorDestroyed,
                HandleActorDestroyedEvent);
        }

        #region Event Handlers

        private void HandleThrustStartedEvent(ITestudinEvent testudinEvent)
        {
            ActorAcceleratingEvent actorThrustStartedEvent = testudinEvent as ActorAcceleratingEvent;

            if (actorThrustStartedEvent == null)
            {
                throw new ArgumentException("Expected ActorThrustStartedEvent");
            }

            Actor actor = GetActor(actorThrustStartedEvent.ActorId);

            PhysicsComponent physicsComponent = actor.GetComponent<PhysicsComponent>();

            if (physicsComponent == null)
            {
                throw new Exception("Expected a PhysicsComponent");
            }

            physicsComponent.ApplyAcceleration(actorThrustStartedEvent.Newtons);
        }

        private void HandleThrustEndedEvent(ITestudinEvent testudinEvent)
        {
            ActorDeceleratingEvent actorThrustEndedEvent = testudinEvent as ActorDeceleratingEvent;

            if (actorThrustEndedEvent == null)
            {
                throw new ArgumentException("Expected ActorThrustEndedEvent");
            }

            Actor actor = GetActor(actorThrustEndedEvent.ActorId);

            if (actor != null)
            {
                PhysicsComponent physicsComponent = actor.GetComponent<PhysicsComponent>();

                if (physicsComponent == null)
                {
                    throw new Exception("Expected a PhysicsComponent");
                }

                physicsComponent.RemoveAcceleration();
            }
        }

        private void HandleActorMovingEvent(ITestudinEvent testudinEvent)
        {
            ActorMovingEvent actorMovingEvent = testudinEvent as ActorMovingEvent;

            if (actorMovingEvent == null)
            {
                throw new ArgumentException("Expected ActorThrustEndedEvent");
            }

            Actor actor = GetActor(actorMovingEvent.ActorId);

            PhysicsComponent physicsComponent = actor.GetComponent<PhysicsComponent>();

            if (physicsComponent == null)
            {
                throw new Exception("Expected a PhysicsComponent");
            }

            TransformComponent transformComponent = actor.GetComponent<TransformComponent>();

            if (transformComponent == null)
            {
                throw new Exception("Expected a TransformComponent");
            }

            Vector3 currentVelocity = physicsComponent.GetVelocity();

            Vector3 direction =
                actorMovingEvent.Direction == MovementDirection.Forward
                ? Vector3.Normalize(transformComponent.Transform.Forward)
                : actorMovingEvent.Direction == MovementDirection.Right
                    ? Vector3.Normalize(transformComponent.Transform.Right)
                    : actorMovingEvent.Direction == MovementDirection.Backward
                        ? Vector3.Normalize(transformComponent.Transform.Backward)
                        : Vector3.Normalize(transformComponent.Transform.Left);

            Vector3 newVelocity = currentVelocity + (direction * MOVEMENT_SPEED);

            if (newVelocity.Length() > MAX_SPEED)
            {
                newVelocity -= (direction * MOVEMENT_SPEED);
            }

            physicsComponent.SetVelocity(newVelocity);
        }

        private void HandleActorRotatingEvent(ITestudinEvent testudinEvent)
        {
            ActorRotatingEvent actorRotatingEvent = testudinEvent as ActorRotatingEvent;

            Actor actor = GetActor(actorRotatingEvent.ActorId);

            TransformComponent transformComponent = actor.GetComponent<TransformComponent>();

            actor.SetComponent<IAnimationComponent>(
                new RotationAnimationComponent(
                    actor,
                    actorRotatingEvent.Direction == RotationDirection.CounterClockwise
                        ? transformComponent.Transform.Left
                        : transformComponent.Transform.Right,
                    _eventManager,
                    3000f));
        }

        private void HandleActorStoppedEvent(ITestudinEvent testudinEvent)
        {
            ActorStoppedEvent actorStoppedEvent = testudinEvent as ActorStoppedEvent;

            if (actorStoppedEvent == null)
            {
                throw new ArgumentException("Expected ActorStoppedEvent");
            }

            Actor actor = GetActor(actorStoppedEvent.ActorId);

            PhysicsComponent physicsComponent = actor.GetComponent<PhysicsComponent>();

            if (physicsComponent == null)
            {
                throw new Exception("Expected a PhysicsComponent");
            }

            physicsComponent.SetVelocity(
                new Vector3(0, 0, 0));
        }

        private void HandleActorSpawningEvent(ITestudinEvent testudinEvent)
        {
            ActorSpawningEvent actorSpawnedEvent = testudinEvent as ActorSpawningEvent;

            Actor actor = _actorSpawner.SpawnActor(
                actorSpawnedEvent.Type,
                actorSpawnedEvent.Transform);

            if (actorSpawnedEvent.Velocity != Vector3.Zero)
            {
                PhysicsComponent physicsComponent = actor.GetComponent<PhysicsComponent>();

                physicsComponent.ApplyForce(actorSpawnedEvent.Velocity, .6f);
            }

            AddActor(actor);
        }

        private void HandleCollisionEvent(ITestudinEvent testudinEvent)
        {
            CollisionEvent collisionEvent = testudinEvent as CollisionEvent;

            Actor actor1 = this.GetActor(collisionEvent.ActorId1);

            if (actor1 != null)
            {
                ICollisionHandlerComponent collisionHandlerComponent1 = actor1.GetComponent<ICollisionHandlerComponent>();

                if (collisionHandlerComponent1 != null)
                {
                    collisionHandlerComponent1.HandleCollision();
                }
            }

            Actor actor2 = this.GetActor(collisionEvent.ActorId2);

            if (actor2 != null)
            {
                ICollisionHandlerComponent collisionHandlerComponent2 = actor2.GetComponent<ICollisionHandlerComponent>();

                if (collisionHandlerComponent2 != null)
                {
                    collisionHandlerComponent2.HandleCollision();
                }
            }
        }

        private void HandleExplosionEvent(ITestudinEvent testudinEvent)
        {
            ExplosionEvent explosionEvent = testudinEvent as ExplosionEvent;

            _processManager.AttachProcess(
                new ExplosionProcess(
                    explosionEvent.Actor,
                    explosionEvent.Radius,
                    explosionEvent.Force,
                    GetAllActors,
                    _actorSpawner,
                    _scene.FindActorSceneNode,
                    explosionEvent.Damage,
                    _eventManager));
        }

        private void HandleActorDestroyedEvent(ITestudinEvent testudinEvent)
        {
            ActorDestroyedEvent actorDestroyedEvent = testudinEvent as ActorDestroyedEvent;

            _actorList.RemoveAll(x => x.Id == actorDestroyedEvent.ActorId);
        }

        #endregion

        private void AddActor(Actor actor)
        {
            _actorList.Add(actor);
        }

        private Actor GetActor(ActorId actorId)
        {
            return _actorList.SingleOrDefault(x => x.Id == actorId);
        }

        private IEnumerable<Actor> GetAllActors()
        {
            return _actorList.AsEnumerable();
        }

        private Actor BuildFloor()
        {
            ActorId floorActorId = ActorIdGenerator.GetNextActorId();

            Actor floorActor = new Actor(floorActorId);

            floorActor.SetComponent<TransformComponent>(
                new TransformComponent(
                    floorActor,
                    Matrix.CreateTranslation(
                        new Vector3(
                            0,
                            -.5f,
                            0))));

            Effect riemersEffect = _contentManager.Load<Effect>("Content/Effects/RiemersEffects");

            VertexPositionColorNormal backLeft = new VertexPositionColorNormal()
            {
                Position = new Vector3(-FLOOR_DIMENSIONS, 0, FLOOR_DIMENSIONS),
                Normal = new Vector3(0, 0, 1),
                Color = Color.Black
            };

            VertexPositionColorNormal frontLeft = new VertexPositionColorNormal()
            {
                Position = new Vector3(-FLOOR_DIMENSIONS, 0f, -FLOOR_DIMENSIONS),
                Normal = new Vector3(0, 0, 1),
                Color = Color.Black
            };

            VertexPositionColorNormal frontRight = new VertexPositionColorNormal()
            {
                Position = new Vector3(FLOOR_DIMENSIONS, 0f, -FLOOR_DIMENSIONS),
                Normal = new Vector3(0, 0, 1),
                Color = Color.Black
            };

            VertexPositionColorNormal backRight = new VertexPositionColorNormal()
            {
                Position = new Vector3(FLOOR_DIMENSIONS, 0f, FLOOR_DIMENSIONS),
                Normal = new Vector3(0, 0, 1),
                Color = Color.Black
            };

            int[] indices = new int[6]
            {
                0,
                3,
                1,
                1,
                3,
                2
            };

            floorActor.SetComponent<IRenderComponent>(
                new PrimitiveRenderComponent(
                    floorActor,
                    RenderPass.DynamicActor,
                    riemersEffect,
                    new VertexPositionColorNormal[4]
                    {
                        backLeft,
                        frontLeft,
                        frontRight,
                        backRight
                    },
                    indices,
                    _eventManager));

            _physicsSimulator.AddCube(
                new Vector3(
                    FLOOR_DIMENSIONS,
                    0,
                    FLOOR_DIMENSIONS),
                0f,
                floorActor);

            return floorActor;
        }

        private Actor CreatePrimitiveActor()
        {
            ActorId primitiveActorId = ActorIdGenerator.GetNextActorId();

            Actor primitiveActor = new Actor(primitiveActorId);

            primitiveActor.SetComponent<TransformComponent>(
                new TransformComponent(
                    primitiveActor,
                    Matrix.CreateTranslation(
                        new Vector3(
                            0,
                            0,
                            0))));

            Effect riemersEffect = _contentManager.Load<Effect>("Content/Effects/RiemersEffects");

            VertexPositionColorNormal top = new VertexPositionColorNormal()
            {
                Position = new Vector3(0f, 1f, 0f),
                Normal = new Vector3(0, 0, 1)
            };

            VertexPositionColorNormal frontLeft = new VertexPositionColorNormal()
            {
                Position = new Vector3(-1f, 0f, 1f),
                Color = Color.Red,
                Normal = new Vector3(0, 0, 1)
            };

            VertexPositionColorNormal frontRight = new VertexPositionColorNormal()
            {
                Position = new Vector3(1f, 0f, 1f),
                Color = Color.Blue,
                Normal = new Vector3(0, 0, 1)
            };


            VertexPositionColorNormal backRight = new VertexPositionColorNormal()
            {
                Position = new Vector3(1f, 0f, -1f),
                Color = Color.White,
                Normal = new Vector3(0, 0, 1)
            };

            VertexPositionColorNormal backLeft = new VertexPositionColorNormal()
            {
                Position = new Vector3(-1f, 0f, -1f),
                Color = Color.Green,
                Normal = new Vector3(0, 0, 1)
            };

            int[] indices = new int[12]
            {
                0,
                1,
                2,
                0,
                2,
                3,
                0,
                3,
                4,
                0,
                1,
                4
            };

            primitiveActor.SetComponent<IRenderComponent>(
                new PrimitiveRenderComponent(
                    primitiveActor,
                    RenderPass.DynamicActor,
                    riemersEffect,
                    new VertexPositionColorNormal[5]
                    {
                        top,
                        frontLeft,
                        frontRight,
                        backRight,
                        backLeft
                    },
                    indices,
                    _eventManager));

            return primitiveActor;
        }

        private void SetCamera(ActorId targetActorId)
        {
            ActorId cameraActorId = ActorIdGenerator.GetNextActorId();

            Matrix projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
                MathHelper.PiOver4,
                (float)_viewPortWidth / _viewPortHeight,
                .01f,
                1000000);

            ICameraSceneNode floatingCameraNode = new FloatingCameraSceneNode(
                projectionMatrix,
                Matrix.CreateTranslation(
                    new Vector3(
                        0,
                        0,
                        12)));

            ISceneNode primitiveSceneNode = _scene.FindActorSceneNode(targetActorId);

            ICameraSceneNode thirdPersonCameraNode = new ThirdPersonCameraSceneNode(
                cameraActorId,
                projectionMatrix,
                primitiveSceneNode,
                new Vector3(
                    0,
                    0,
                    6));

            _scene.Camera = floatingCameraNode;
        }

        private void DisposeGameViews()
        {
            foreach (IGameView currentGameView in _gameViews)
            {
                currentGameView.Dispose();
            }

            _gameViews.Clear();
        }

        private void DisposeActors()
        {
            foreach (Actor currentActor in _actorList)
            {
                currentActor.Dispose();
            }

            _actorList.Clear();
        }

        #endregion
    }
}
